﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Printing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml.Linq;
using System.Xml.Serialization;
using Dycox.ComponentModel;
using Dycox.Reporting.Printing;

namespace Dycox.Reporting
{
    public class XmlReportLoader
    {
        public static readonly XNamespace ReportNamespace = "urn:effun-net-dycox-report";

        public XmlReportLoader()
            : this(typeof(Report))
        {
        }

        public XmlReportLoader(Type reportType)
        {
            if (reportType == null)
                throw new ArgumentNullException("reportType");

            if (reportType.IsAbstract || reportType != typeof(Report) && !reportType.IsSubclassOf(typeof(Report)) || reportType.GetConstructor(new Type[] { typeof(string) }) == null)
                throw new ArgumentException("Invalid report type.");

            _reportType = reportType;

        }

        Report _report;
        Type _reportType;

        public Report LastReportLoaded
        {
            get { return _report; }
        }

        private void ValidateSchema(XElement root)
        {
            if (root == null)
                throw new ArgumentNullException("root");

            XName name = root.Name;

            if (name.Namespace != ReportNamespace)
                throw new ArgumentException("Invalid root element.");

            if (name.LocalName == "report")
            {
                return;
            }
            else if (name.LocalName == "reports")
            {
                throw new NotSupportedException("Multi report is no longer supported.");
            }
            else
                throw new ArgumentException("Invalid root element.");
        }


        private Report LoadReport(XElement root)
        {
            ValidateSchema(root);

            XElement xe;
            Report report;

                string text = (string)root.Attribute("name");
                report = (Report)Activator.CreateInstance(_reportType, text); 

                report.Description = (string)root.Element(ReportNamespace + "description");

                if ((xe = root.Element(ReportNamespace + "variables")) != null)
                    LoadVariables(xe, report);

                if ((xe = root.Element(ReportNamespace + "pageSettings")) != null)
                    LoadPageSettings(xe, report);

                if ((xe = root.Element(ReportNamespace + "body")) != null)
                    LoadReportElements(xe, report);

            if (!object.ReferenceEquals(_report, report))
            {
                _report = report;
            }

            return report;
        }

        public Report Load(TextReader reader)
        {
            XDocument doc = XDocument.Load(reader);
            return LoadReport(doc.Root);
        }

        public Report Load(string path)
        {
            using (TextReader reader = File.OpenText(path))
            {
                return Load(reader);
            }
        }

        public Report LoadXml(string xml)
        {
            using (StringReader reader = new StringReader(xml))
            {
                return Load(reader);
            }
        }

        public Report Load(Type type, string resName)
        {
            if (type == null)
                throw new ArgumentNullException("type");

            Assembly asm = type.Assembly;

            string[] names = asm.GetManifestResourceNames();
            if (!names.Any(o => o == resName))
            {
                resName = type.Namespace + Type.Delimiter + resName;
            }

            return Load(asm, resName);
        }

        public Report Load(Assembly assembly, string resName)
        {
            if (assembly == null)
                throw new ArgumentNullException("assembly");
            if (resName == null)
                throw new ArgumentNullException("resName");

            Stream stream = assembly.GetManifestResourceStream(resName);
            if (stream == null)
                throw new ArgumentException("Resource " + resName + " not found in " + assembly);

            using (StreamReader reader = new StreamReader(stream))
            {
                return Load(reader);
            }

        }

        private void LoadVariables(XElement xeVariables, Report report)
        {
            XName xname = ReportNamespace + "var";

            foreach (XElement xe in xeVariables.Elements().Where(o => o.Name == xname))
            {
                string value = xe.GetAttrString("value");
                if (!string.IsNullOrEmpty(value))
                {
                    string typeName = xe.GetAttrString("type");
                    Type type;

                    if (!string.IsNullOrEmpty(typeName))
                        type = Type.GetType(typeName, true);
                    else
                        type = typeof(string);

                    report.SetVariable(xe.GetAttrString("name"), Dycox.Utils.ConvertUtil.ConvertType(value, type));
                }
            }
        }

        //private void InitializePaperSize(ReportPageSettings rps, string paperName, float width, float height, ReportPageUnit unit)
        //{
        //    PaperSize paperSize;

        //    while (true)
        //    {
        //        paperSize = GetPaperSize(paperName);

        //        SizeF desiredSize = new SizeF(width, height);
        //        SizeF actualSize;
        //        PaperNotFoundEventArgs e;

        //        if (paperSize != null)
        //        {
        //            actualSize = ConvertSize(paperSize, unit);
        //            if (desiredSize.ToSize() != actualSize.ToSize())
        //                e = new PaperNotFoundEventArgs(_printerName, paperName, desiredSize, unit, PaperNotFoundReason.SizeNotMatch);
        //            else
        //                e = null;
        //        }
        //        else
        //            e = new PaperNotFoundEventArgs(_printerName, paperName, desiredSize, unit, PaperNotFoundReason.NotDefined);

        //        if (e != null)
        //        {
        //            OnPaperNotFound(e);
        //            if (!e.Retry) break;
        //        }
        //        else
        //            break;
        //    }

        //    if (paperSize != null)
        //    {
        //        rps.PaperKind = paperSize.Kind;
        //        rps.PaperName = paperSize.PaperName;
        //    }
        //    else
        //        throw new InvalidPaperNameException(paperName);

        //    rps.PaperSize = ConvertSize(paperSize, unit);
        //}

        private void LoadPageSettings(XElement xePageSettings, Report report)
        {
            XElement xe = xePageSettings.Element(ReportNamespace + "paper");
            ReportPageUnit unit = xePageSettings.GetAttrEnum<ReportPageUnit>("unit");
            ReportPageSettings rps = report.PageSettings;
            string text;

            rps.PaperName= (string)xe.Attribute("name");
            rps.PaperSize = new SizeF(
                ((float?)xe.Attribute("width")).GetValueOrDefault(),
                ((float?)xe.Attribute("height")).GetValueOrDefault());

            rps.Landscape = xe.GetAttrBoolean("landscape").GetValueOrDefault();
            rps.Unit = unit;
            rps.Columns = ((int?)xePageSettings.Attribute("columns")).GetValueOrDefault(1);
            rps.Rows = ((int?)xePageSettings.Attribute("rows")).GetValueOrDefault();
            rps.PageBreak = xePageSettings.GetAttrEnum<PageBreakMode>("pageBreak");
            rps.VerticalOverflow = xePageSettings.GetAttrEnum<OverflowHandling>("overflow-v");
            rps.HorizontalOverflow = xePageSettings.GetAttrEnum<OverflowHandling>("overflow-h");
            rps.VerticalSpace = ((int?)xePageSettings.Attribute("space-v")).GetValueOrDefault();
            rps.HorizontalSpace = ((int?)xePageSettings.Attribute("space-h")).GetValueOrDefault();

            text = (string)xe.Attribute("margins");
            if (text != null) rps.Margins = Dycox.Drawing.Paddings.Parse(text);

            //xe = xePageSettings.Element(ReportNamespace + "margins");
            //if (xe != null)
            //{
            //    ReportMargins margins = new ReportMargins();
            //    margins.Left = xe.GetAttrSingle("left").GetValueOrDefault();
            //    margins.Top = xe.GetAttrSingle("top").GetValueOrDefault();
            //    margins.Right = xe.GetAttrSingle("right").GetValueOrDefault();
            //    margins.Bottom = xe.GetAttrSingle("bottom").GetValueOrDefault();
            //    rps.Margins = margins;
            //}
        }


        //private string _printerName;

        //public string PrinterName
        //{
        //    get { return _printerName; }
        //}

        //public void ChangePrinter(string printerName)
        //{
        //    _printerName = printerName;
        //    if ((_partsLoaded & ReportParts.PageSettings) == ReportParts.PageSettings)
        //    {
        //        _partsLoaded &= ~ReportParts.PageSettings;
        //        if (_report != null)
        //        {
        //            var rps = _report.PageSettings;
        //            InitializePaperSize(rps, rps.PaperName, rps.PaperSize.Width, rps.PaperSize.Height, rps.Unit);
        //        }
        //    }
        //}

        //private PaperSize GetPaperSize(string name)
        //{

        //    PrinterSettings printer = new PrinterSettings();
        //    if (_printerName != null)
        //    {
        //        try { printer.PrinterName = _printerName; }
        //        catch { }
        //    }

        //    if (string.IsNullOrEmpty(name))
        //        return printer.DefaultPageSettings.PaperSize;

        //    foreach (PaperSize size in printer.PaperSizes)
        //    {
        //        if (size.PaperName == name)
        //            return size;
        //    }
            
            
        //    return null;
        //}


        private void LoadReportElements(XElement xeBody, Report report)
        {
            ReadElementProperties(report.Body, xeBody);
            LoadReportElements(xeBody, report.Body);
        }

        private void LoadReportElements(XElement xeContainer, ContainerElement container)
        {
            foreach (XElement xe in xeContainer.Elements().Where(o => o.Name.Namespace == ReportNamespace))
            {
                string name = xe.Name.LocalName;
                ReportElement re ;

                switch (name)
                {
                    case "textBlock":
                        re = new TextBlock();
                        break;
                    case "container":
                        re = new ContainerElement();
                        break;
                    case "pictureBox":
                        re = new PictureBox();
                        break;
                    case "line":
                        re = new Line();
                        break;
                    case "table":
                        re = new Table();
                        break;
                    case "dataSheet":
                        re = new DataSheet();
                        break;
                    default:
                        re = null;
                        break;
                }

                if (re != null)
                {
                    ReadElementProperties(re, xe);
                    container.Elements.Add(re);
                    if (re is ContainerElement)
                        LoadReportElements(xe, (ContainerElement)re);
                }
            }
        }

        private void ReadElementProperties(ReportElement re, XElement xe)
        {
            DeserializeProperties(re, xe);

            XElement xstyle = xe.Element(ReportNamespace + "style");
            if (xstyle != null)
            {
                ReadStyles(re.Style, xstyle);
            }
        }

        private void DeserializeProperties(object obj, XElement xe)
        {
            foreach (PropertyDescriptor pinfo in TypeDescriptor.GetProperties(obj))
            {
                LoaderContext context = new LoaderContext(obj, pinfo);
                if (!TryDeserializeAttribute(context, xe) && !TryDeserializeElement(context, xe))
                    TryDeserializeCollection(context, xe);
            }
        }

        private bool TryDeserializeAttribute(LoaderContext context, XElement xe)
        {
            PropertyDescriptor pinfo = context.PropertyDescriptor;
            XmlAttributeAttribute xaa = (XmlAttributeAttribute)pinfo.Attributes[typeof(XmlAttributeAttribute)];
            if (xaa != null)
            {
                string name;
                name = xaa.AttributeName ?? pinfo.Name;

                XAttribute xa = xe.Attribute(name);
                if (xa != null)
                {
                    string value = xa.Value;
                    context.SetValue(value);
                }
                return true;
            }

            return false;
        }

        private bool TryDeserializeElement(LoaderContext context, XElement xe)
        {
            PropertyDescriptor pinfo = context.PropertyDescriptor;

            XmlElementAttribute xea = (XmlElementAttribute)pinfo.Attributes[typeof(XmlElementAttribute)];
            if (xea != null)
            {
                string name;
                name = xea.ElementName ?? pinfo.Name;

                XElement xa = xe.Element(ReportNamespace + name);
                if (xa != null)
                {
                    object inst = context.GetValue();

                    if (inst == null)
                    {
                        if (pinfo.IsReadOnly)
                            throw new InvalidOperationException("unable_to_assign_value");

                        inst = Activator.CreateInstance(pinfo.PropertyType);
                        context.SetValue(inst);
                    }

                    if (inst is ElementStyle)
                        ReadStyles((ElementStyle)inst, xa);
                    else
                        DeserializeProperties(inst, xa);
                }

                return true;
            }

            return false;
        }
        
        private bool TryDeserializeCollection(LoaderContext context, XElement xe)
        {
            PropertyDescriptor p = context.PropertyDescriptor;

            if (p.PropertyType.GetInterface("IList") == typeof(IList))
            {
                XmlArrayAttribute xaa = (XmlArrayAttribute)p.Attributes[typeof(XmlArrayAttribute)];
                XmlArrayItemAttribute xaia = (XmlArrayItemAttribute)p.Attributes[typeof(XmlArrayItemAttribute)];

                if (xaia != null)
                {
                    string name;
                    IList list = (IList)context.GetValue();
                    bool flag = false;

                    if (list == null)
                    {
                        if ((p.PropertyType.IsArray || p.PropertyType == typeof(IList)) && !p.IsReadOnly)
                        {
                            list = new ArrayList();
                            flag = true;
                        }
                        else
                            throw new InvalidOperationException("Cannot deserialize collection because the value of property " + p.Name + " of " + p.PropertyType.DeclaringType.FullName + " is null and not assignable.");
                    }

                    if (xaa != null)
                    {
                        name = xaa.ElementName ?? p.Name;
                        XElement tmp = xe.Element(ReportNamespace + name);
                        if (tmp == null)
                            return true;
                        xe = tmp;
                    }

                    name = xaia.ElementName ?? p.Name;
                    foreach (XElement xeItem in xe.Elements(ReportNamespace + name))
                    {
                        object item = Activator.CreateInstance(xaia.Type);
                        DeserializeProperties(item, xeItem);
                        list.Add(item);
                    }

                    if (flag)
                        context.SetValue(((ArrayList)list).ToArray(), false);
                }
            }

            return false;
        }

        private void ReadStyles(ElementStyle style, XElement xeStyle)
        {
            ReadStyleProperties(style, xeStyle);

            foreach (XElement xe in xeStyle.Elements().Where(o => o.Name.Namespace == ReportNamespace))
            {
                string name = xe.Name.LocalName;
                Style child;

                switch (name)
                {
                    case "font":
                        child = style.Font;
                        break;
                    case "border":
                        child = style.Border;
                        break;
                    case "background":
                        child = style.Background;
                        break;
                    default:
                        child = null;
                        break;
                }

                if (child != null)
                {
                    ReadStyleProperties(child, xe);
                }
            }
            
        }

        private void ReadStyleProperties(Style style, XElement xeStyle)
        {
            foreach (XAttribute attr in xeStyle.Attributes())
            {
                string value = attr.Value;

                if (!string.IsNullOrEmpty(value))
                    style.SetValueCore(attr.Name.LocalName, value);
            }
        }

        class LoaderContext : SimpleTypeDescriptorContext
        {
            public LoaderContext(object instance, PropertyDescriptor property)
                : base(instance, property)
            {

            }
        }
    }
}
